package de.gwtprime.shared.util;

import java.util.Date;


/**
 * Helper class to convert between units
 * 
 */
public class UnitConverter
{
	/**
	 * The different types of supported units
	 */
	private static enum UnitType
	{
		STORAGE_1000,
		STORAGE_1024,
		CPU_FREQUENCY,
		TIME;
	};
	
	/**
	 * Enum to manage different unit types
	 * 
	 * Note that there are different units for storage measures: xxx1024 and xxx1000
	 * @author as
	 */
	public static enum Unit
	{
		BIT_1024			("Bit", UnitType.STORAGE_1024, 	1d), 
		BYTE_1024			("Byte", UnitType.STORAGE_1024, 8d), 
		KILOBYTE_1024		("kB", UnitType.STORAGE_1024, 	8d * 1024d), 
		MEGABYTE_1024		("MB", UnitType.STORAGE_1024, 	8d * 1024d * 1024d), 
		GIGABYTE_1024		("GB", UnitType.STORAGE_1024, 	8d * 1024d * 1024d * 1024d), 
		TERABYTE_1024		("TB", UnitType.STORAGE_1024, 	8d * 1024d * 1024d * 1024d * 1024d), 
		PETABYTE_1024		("PB", UnitType.STORAGE_1024, 	8d * 1024d * 1024d * 1024d * 1024d * 1024d),
		BIT_1000			("Bit", UnitType.STORAGE_1000, 	1d), 
		BYTE_1000			("Byte", UnitType.STORAGE_1000, 8d), 
		KILOBYTE_1000		("kB", UnitType.STORAGE_1000, 	8d * 1000d), 
		MEGABYTE_1000		("MB", UnitType.STORAGE_1000, 	8d * 1000d * 1000d), 
		GIGABYTE_1000		("GB", UnitType.STORAGE_1000, 	8d * 1000d * 1000d * 1000d), 
		TERABYTE_1000		("TB", UnitType.STORAGE_1000, 	8d * 1000d * 1000d * 1000d * 1000d), 
		PETABYTE_1000		("PB", UnitType.STORAGE_1000, 	8d * 1000d * 1000d * 1000d * 1000d * 1000d),
		HERTZ				("Hz", UnitType.CPU_FREQUENCY, 	1d),
		KILOHERTZ			("kHz", UnitType.CPU_FREQUENCY, 1000d), 
		MEGAHERTZ			("MHz", UnitType.CPU_FREQUENCY, 1000d * 1000d), 
		GIGAHERTZ			("GHz", UnitType.CPU_FREQUENCY, 1000d * 1000d * 1000d), 
		TERAHERTZ			("THz", UnitType.CPU_FREQUENCY, 1000d * 1000d * 1000d * 1000d),
		MILLISECONDS		("ms", "ms", UnitType.TIME, 			1 / 1000d), 
		SECONDS				("sekunde", "sekunden", UnitType.TIME, 			1d), 
		MINUTES				("minute", "minuten",  UnitType.TIME, 			60d), 
		HOURS				("stunde", "stunden", UnitType.TIME, 			60d * 60d), 
		DAYS				("tag", "tage", UnitType.TIME, 		60d * 60d * 24d);
		
		Unit(String unitString, UnitType unitType, double factorFromBaseUnit)
		{
			this.unitString = unitString;
			this.unitStringPlural = unitString;
			this.unitType = unitType;
			this.factorFromBaseUnit = factorFromBaseUnit;
		}
		
		Unit(String unitString, String unitStringPlural, UnitType unitType, double factorFromBaseUnit)
		{
			this.unitString = unitString;
			this.unitStringPlural = unitStringPlural;
			this.unitType = unitType;
			this.factorFromBaseUnit = factorFromBaseUnit;
		}
		
		private String unitString = null;
		private String unitStringPlural = null;
		private UnitType unitType = null;
		
		// Type is double because this is the primitive type with biggest range + allows decimals
		private double factorFromBaseUnit;

		public double getMultiplier() { return factorFromBaseUnit; }
		
		public String getUnitString() { return unitString; 	}
		
		/**
		 * @param input
		 * @return The value of an ammount of "input" of type "Unit.this", in its base unit
		 */
		public double toBaseUnit(double input)
		{
			return input * getMultiplier();
		}

	

		/**
		 * returns true if this unit is a time unit
		 * 
		 * @return
		 */
		public boolean isTimeUnit()
		{
			return unitType == UnitType.TIME;
		}
		
		public UnitType getUnitType()
		{
			return unitType;
		}

		/**
		 * Lookup unit by unit symbol (case-insensitive).
		 * 
		 * @param unitText unit symbol, e.g. "kB"
		 * 
		 * @return unit or <code>null</code> if unknown
		 */
		public static Unit byUnit(String unitText) {
			for (Unit unit : values()) {
				if (unitText.equalsIgnoreCase(unit.getUnitString())) {
					return unit;
				}
			}
			return null;
		}
	}

	/**
	 * Converts the double input value (which is provided in inputUnit) into the
	 * desired output unit and returns a String representation
	 * 
	 * e.g. convertInputToAsString( 1.0, Unit.MEGABYTE_1024, Unit.KILOBYTE_1024)
	 * 
	 * will result in "1024.00 kB"
	 * 
	 * @param input
	 * @param inputUnit
	 * @param outputUnit
	 * @return
	 */
	public static String convertInputToAsString(Double input, Unit inputUnit, Unit outputUnit)
	{
		
		if(outputUnit==Unit.SECONDS || outputUnit==Unit.MILLISECONDS)
			return "gerade eben";
		Double result = convertInputToGivenUnit(input, inputUnit, outputUnit);
		return "vor " + NumFormat.formatDouble(result) + " " + (result.intValue()==1 ? outputUnit.unitString : outputUnit.unitStringPlural);
	}
	
	public static Pair<Double,Unit> convertInputFromString(String input)
	{
		return convertInputFromString(input, null);
	}
	
	public static Pair<Double,Unit> convertInputFromString(String input, Unit defaultUnit)
	{
		Unit unit = defaultUnit;
		Double value = null;
		
		if (input == null) {
			return null;
		}
		input = input.trim();
		
		int unitPos = input.length();
		do {
			String numberText = input.substring(0, unitPos);
			try {
				// simple case: no unit
				value = Double.parseDouble(numberText);
				break;
			}
			catch (Throwable t) {
				// stop one character earlier and try again
				unitPos--;
			}
		}
		while (unitPos > 0);
		String unitText = null;
		if (unitPos < input.length()) {
			unitText = input.substring(unitPos);
		}
		if (unitText != null) {
			unitText = unitText.trim();
			unit = Unit.byUnit(unitText);
		}
		
		if (unit == null) {
			unit = defaultUnit;
		}
		
		return new Pair<Double, Unit>(value, unit);
	}
	
	/**
	 * 
	 * @param input
	 * @param inputUnit
	 * @return value and unit (inputFitting)
	 */
	public static Pair<Double,Unit> getValueAndUnitFitting(double input, Unit inputUnit)
	{
		// Start with initual value / unit
		Double resultValue = input;
		Unit resultUnit = inputUnit;
		
		for (Unit tempUnit: Unit.values())
		{
			// Only test units of the same type
			if (tempUnit.getUnitType() == inputUnit.getUnitType())
			{
				Double tempVal = convertInputToGivenUnit(input, inputUnit, tempUnit);
				
				// Find output unit that shows smallest value bigger or equal to 1 (22.03 GB, etc; values like 0.23 MB are filtered out)
				if (tempVal < resultValue && tempVal >= 1)
				{
					resultValue = tempVal;
					resultUnit = tempUnit;
				}
			}
		}
		return new Pair<Double,Unit>(resultValue, resultUnit);
	}

	
	
	public static String convertInputFitting(Date date){
		
		
		return convertInputFitting(new Date().getTime()-date.getTime(), Unit.MILLISECONDS);
		
	}
	/**
	 * Finds output unit with shortest string representation
	 * 
	 * @param input
	 * @param inputUnit
	 * @param outputUnit
	 * @return shortest string representation, with value >=1
	 */
	public static String convertInputFitting(double input, Unit inputUnit)
	{
		// Start with initual value / unit
		Unit resultUnit = getValueAndUnitFitting(input, inputUnit).snd;
		
		// Convert to string
		return convertInputToAsString(input, inputUnit, resultUnit);
	}

	public static String convertInputFitting(Long input, Unit inputUnit)
	{
		//if value is null --> return n/a
		if(input == null)
			return "n/a";
		return convertInputFitting(new Double(input.longValue()), inputUnit);
	}

	/**
	 * Converts the input double value (which is provided in inputUnit) into the
	 * desired output unit
	 * 
	 * @param inputValue
	 * @param inputUnit
	 * @param outputUnit
	 * @return
	 */
	public static Double convertInputToGivenUnit(Double inputValue, Unit inputUnit, Unit outputUnit)
	{
		if (inputUnit == outputUnit)
			return inputValue;
		
		// First multiply find the baseValue, then divide to find the result => minimize precision loss
		Double baseValue = inputUnit.toBaseUnit(inputValue);
		Double resultValue = baseValue / outputUnit.getMultiplier();
		return resultValue;
	}

	/**
	 * Helper function to round the input double to the specified number of
	 * digits
	 * @param inputDouble
	 * @param numberDigits
	 * @return
	 */
	public static Double roundToNumberDigits(Double inputDouble, int numberDigits)
	{
		return Math.round(inputDouble * Math.pow(10, numberDigits)) / Math.pow(10, numberDigits);
	}
	
   

	/**
	 * @param time
	 * @param inputUnit 
	 * @return string representation of the specified time. The result will have millisecond granularity.
	 * 
	 * @see #convertTimeFitting(long, Unit, Unit)
	 */
	public static String convertTimeFitting(long time, Unit inputUnit)
	{
		return convertTimeFitting(time, inputUnit, Unit.MILLISECONDS);
	}
	
	/**
	 * @param time
	 * @param inputUnit
	 * @param outputGranularity Smallest unit visible in the output. What is smaller than that is "rounded off".
	 * @return String representation of the time given in inputUnits. OutputGranularity defines the smallest unit visible in the output.
	 * For example, for time=65, inputUnit=Seconds, outputGranularity=Minute, the result will be 1m (the extra 5s will be rounded off)
	 * 
	 * @see #convertTimeFitting(long, Unit)
	 */
	public static String convertTimeFitting(long time, Unit inputUnit, Unit outputGranularity)
	{
		// If trying to achieve a greater granularity than the input unit, just return the value in the input unit
		if (outputGranularity.getMultiplier() < inputUnit.getMultiplier())
			return time + " " + inputUnit.getUnitString();
		
		// TODO Use double instead of long (use convertInputTo to convert to m, s, h, etc)
		long d = 0, h = 0, m = 0, s = 0, ms = 0;
		switch (inputUnit)
		{
			case MILLISECONDS:
				if (time / 1000 / 60 / 60 >= 1)
				{
					h = time / 1000 / 60 / 60;
					m = (time - (h * 1000 * 60 * 60)) / 1000 / 60;
					s = (time - (h * 1000 * 60 * 60) - (m * 1000 * 60)) / 1000;
					ms = (time - (h * 1000 * 60 * 60) - (m * 1000 * 60) - (s * 1000));
				}
				else if (time / 1000 / 60 >= 1)
				{
					m = time / 1000 / 60;
					s = (time - m * 1000 * 60) / 1000;
					ms = (time - m * 1000 * 60 - s * 1000);
				}
				else if (time / 1000 >= 1)
				{
					s = time / 1000;
					ms = time - s * 1000;
				}
				else
					ms = time - s * 1000;
				break;

			case SECONDS:
				if (time / 60 / 60 >= 1)
				{
					h = time / 60 / 60;
					m = (time - (h * 60 * 60)) / 60;
					s = (time - (h * 60 * 60) - (m * 60));
				}
				else if (time / 60 >= 1)
				{
					m = time / 60;
					s = (time - m * 60);
				}
				else
					s = time;
				break;

			case MINUTES:
				if (time / 60 >= 1)
				{
					h = time / 60;
					m = (time - (h * 60));
				}
				else
					m = time;
				break;

			case HOURS:
				h = time;
				break;

			default:
				return "Time calculation failed!";
		}
		
		// Compute days, adjust hours left
		d = h / 24;
		h = h % 24;
		
		String result = "";
		switch (outputGranularity)
		{
			// Append the valid units as prefixes, thus building up from lower to higher units
			case MILLISECONDS: result = (ms > 0 ? ms + "ms " : "") + result;
			case SECONDS: result = (s > 0 ? s + "s " : "") + result;
			case MINUTES: result = (m > 0 ? m + "m " : "") + result;
			case HOURS: result = (h > 0 ? h + "h " : "") + result;
			case DAYS: result = (d > 0 ? d + "d " : "") + result;
		}
		
		return result;
	}
	
	
}
